Add a sparse keyword to reductions along a dimension and reduce column-range views through the sparse kernels - #798
ViralBShah wants to merge 5 commits into
Conversation
…atrix Fixes #43. `sum(A; dims)` and the other dimensional reductions of a `SparseMatrixCSC` returned a dense `Matrix`, which for a hypersparse matrix costs O(size) time and memory for a result with a handful of entries. They now return a `SparseMatrixCSC` of the reduced shape that stores an entry only for the rows or columns that store one themselves, unless the reduction of a structurally empty slice is nonzero, in which case the result is fully stored. Results without a `zero`, such as the tuples of `extrema`, stay dense. `reducedim_initarray` provides a structurally empty destination when the initial value is zero and a fully stored one otherwise. A new `_mapreducedim!` for a sparse destination reduces a fully stored one as the dense array its values form, fills an empty one without touching the slices that store nothing, and sends anything in between through the element-wise kernel. Row reductions build the `1 x n` result column by column. Column reductions use the result's value vector as a dense workspace compressed in place when there are enough stored entries, and otherwise sort the stored entries by row so that only rows storing something are visited. Measured on nightly against main, min of 9: `sum(A; dims=2)` for a 10^7 x 150 matrix with 151 entries goes from 6.5 ms and 156 MiB to 2.3 µs and 16 KiB; for 10^4 x 10^4 at 1e-3 and 1e-2 density the reductions are within noise, with the result's extra index vectors as the only added allocation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6EdE4F3CCGE3vxr8gQYKf
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #798 +/- ##
==========================================
+ Coverage 92.53% 92.73% +0.19%
==========================================
Files 12 12
Lines 8404 8819 +415
==========================================
+ Hits 7777 8178 +401
- Misses 627 641 +14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Fixes #377. `sum(view(A, :, j:k); dims)` and the other reductions of a column-range view went through Base's element-wise fallback, indexing the parent once per element. The reduction kernels only need the column pointers, row indices and values, which a `SparseMatrixCSCView` already exposes off the parent's storage, so they now accept `SparseMatrixCSCUnion`. The two fully-stored fast paths index through the column pointers rather than assuming the values start at one, and the hypersparse column reduction sorts the view's stored range. `nnz` of such a view is now O(1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzbyurS9gH2pdqWJrWaPxD
|
I'm coming from a field where the sparse matrices are not hyper-sparse and this obviously feels like a strange default to me. The only case where I see a big benefit of the proposal is when you have many more rows than columns, very low density, and reduce with Instead, the natural choice to me would be to opt-in to this behavior, because in a limited number of situations, it is super useful. |
|
Agree - and the CSC storage format is not a good fit for hypersparse anyways. |
Reductions along a dimension of a sparse matrix return a dense `Matrix` again, as before this PR and as for dense input: the result has one dimension fewer and is usually dense, and downstream code expects it dense. The sparse result is now opt-in by reducing into a sparse destination, `sum!(spzeros(size(A, 1), 1), A)` or `Base.mapreducedim!` and the other in-place reductions, which keeps the hypersparse kernels and their cost proportional to the stored entries plus the length of the result. A destination that stores only zeros, as a reused `sum!` destination does after its `fill!`, folds like an empty one so that reuse stays on the fast path. Column-range views keep reducing off the parent's storage and now return the same dense result as their copy, where Base's `similar` gave them a sparse one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DGaTC2P39YrE2aAU5es55x
|
Made the sparse result opt-in, as suggested. |
…e destination `sum(A; dims = 2, sparse = true)` and the other reductions along a dimension now return a `SparseMatrixCSC`; reducing into a sparse destination is no longer special-cased. Base forwards unknown keywords from `sum`, `prod`, `maximum`, `minimum` and `extrema` to `mapreduce`, so one `mapreduce` method on the sparse types carries the keyword; `any`, `all` and `count` do not forward it and get their own methods. Without a destination the kernels can no longer rely on `sum!` and friends having initialized it, so each slice is now seeded the way Base seeds the dense result: with `init` when given, otherwise with `mapreduce_first` of its first stored entry, and the unstored entries folded in afterwards. The element type and the value of a slice with nothing to reduce come from Base's `reducedim_init` on a stand-in, so they match the dense result exactly, including the widening of small integers and the error for `maximum` over an empty axis. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgWK99c6dYwt3gxH44MxCs
sparse = true; reduce column-range views through the sparse kernels
sparse = true; reduce column-range views through the sparse kernelssparse keyword to reductions along a dimension and reduce column-range views through the sparse kernels
…trim the tests `extrema(A; dims, sparse = true)` failed with a MethodError on `zero(Tuple)`; it now throws an ArgumentError. `any` and `all` with `sparse = true` accept callables that are not `Function`s. The reduction grid in the tests drops three pairs that exercise no new path, and gains complex, callable and `extrema` cases. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fixes #43. Fixes #377.
Issue.
sum(A; dims = 2)on a hypersparse matrix allocates a dense result proportional tosize(A, 1)however few entriesAstores (#43). Reductions of a column-range view went through Base's element-wise fallback and returned a sparse result, since Base'ssimilaron aSubArraydefers to the parent (#377).Fix. The default stays a dense
Matrix, as for dense input (see the discussion below). A sparse result is opt-in:sum(A; dims = 2, sparse = true), and likewise forprod,maximum,minimum,count,any,allandmapreduce, withinitwhere they accept it. It stores an entry for each row or column that stores one, or for every slice when an unstored slice reduces to something nonzero, with the element type of the dense result. Column-range views reduce through the sparse kernels and return the sameMatrixas their copy;nnzof such a view is O(1).Mechanism. Base forwards unknown keywords from
sum,prod,maximumandminimumtomapreduce, so onemapreducemethod carries the keyword;any,allandcountget their own. Slices are seeded as Base seeds the dense result, withinitormapreduce_first, and unstored entries are folded in through the existing_mapreducezeros. Element type and the value of an empty slice come from Base'sreducedim_initon a stand-in. Row reductions build the result column by column; column reductions use a dense workspace, or sort the stored entries by row when there are fewer thanm / 8of them.Dispatch. New
sparse-keyword methods forBase.mapreduce,any,allandcountonSparseMatrixCSCUnion; the existing sparse reduction methods widen fromAbstractSparseMatrixCSCtoSparseMatrixCSCUnion. Behaviour change: reductions along a dimension of a column-range view now return aMatrix.extremawithsparse = truethrows anArgumentError, since a tuple has no zero.Measured on 1.14-DEV, min over samples:
sparse = truesum(A; dims=2), 10^7 x 150, 151 entriessum(B; dims=1), 10^4 x 10^4, 1e-2sum(B; dims=2), 10^4 x 10^4, 1e-2The #377 example, 101 x 99 at density 0.01, columns 5:11: view within 10% of the copy for
sum,maximumandcountalong either dimension, versus the element-wise fallback before.Tests compare every reduction with the dense result over shapes, densities and
dims, for the matrix, its column-range view and withsparse = true, real and complex;init; the stored pattern; element types for small integers andBool; empty dimensions; an allocation bound on the hypersparse path;@whichfor the view kernels; and the errors. Full suite, whitespace and Aqua pass locally on 1.14-DEV. Docs: a paragraph indocs/src/index.md. Not for backport.Left out. Two pre-existing corners:
fill!(spzeros(0, n), x)withx != 0throws in_fillnonzero!, andminimum(spzeros(3, 0); dims = 1)returns a sparse1 x 0result through Base'smap.🤖 Generated with Claude Code
https://claude.ai/code/session_01DGaTC2P39YrE2aAU5es55x
https://claude.ai/code/session_01SgWK99c6dYwt3gxH44MxCs